#!/bin/bash
# 所有shell脚本的测试用例都应该source此文件
# 约定：
#   1、以下划线"_"开头的函数和变量用例不能直接调用
#   2、环境变量全大写，全局变量加上"g_"前置，局部变量统一加"local"修饰

# Func: 判断当前进程某个文件描述符是否指向tty
# Args:
#   $1 -- 句柄编号，例如1-stdout，2-stderr
# Return:
#   0 -- 是tty终端
#   1 -- 非TTY终端
isatty() {
    stat --format="%F" -L "/proc/$$/fd/$1" | grep -iq "^char"
}

# Func: 打印带颜色的字符串，不带换行符
# Args:
#   $1 -- 背景颜色
#   $2 -- 文字颜色
#   $* -- 要打印的内容
_print_color() {
    local _bg="$1"
    shift
    local _fg="$1"
    shift
    if isatty 1; then
        printf "\033[${_bg};${_fg}m%s\033[0m" "$*"
    else
        printf "%s" "$*"
    fi
}
print_red() {
    _print_color 1 31 "$@"
}
print_green() {
    _print_color 1 32 "$@"
}
print_yellow() {
    _print_color 1 33 "$@"
}
print_blue() {
    _print_color 1 34 "$@"
}
print_purple() {
    _print_color 1 35 "$@"
}

# Func: 输出调试级别用例日志
dbg() {
    echo "$@" 1>&2
}

# Func: 输出普通级别用例日志
msg() {
    echo "$@" 1>&2
}

# Func: 输出错误级别用例日志，如果是用例上下文，则标记用例为失败状态
err() {
    echo "$@" 1>&2
    # 只有用例上下文才设置用例状态
    is_function _tc_fail && _tc_fail
}

# Func: 往指定文件写内容
# Args:
#  $1 -- 目标文件
#  $* -- 要写入的文本内容
write_file() {
    local _file="$1"
    shift
    echo "$@" >"${_file}"
}

# Func: 将日志写入到内核ring buffer中
# Args:
#   $* 要写入的日志内容
log_kmsg() {
    [ -w /proc/kmsg ] || return
    write_file /proc/kmsg "$*" 2>/dev/null
}

is_function() {
    type "$1" 2>&1 | head -n 1 | grep "$1 is a function" >/dev/null 2>&1
    return $?
}

is_root() {
    [ "$(id -u)" == "0" ]
}

get_up_time_sec() {
    awk -F . '{print $1}' /proc/uptime
}

get_up_time_ms() {
    local _tst_uptime
    _tst_uptime=$(awk '{print $1}' /proc/uptime | tr -d '.')
    echo -n "${_tst_uptime}0"
}

# 获取时间戳
get_timestamp_sec() {
    date "+%s"
}
get_timestamp_ms() {
    date "+%s%3N"
}

# 输出时间差，并将ms转换为sec
# $1 -- start time: ms
# $2 -- end time: ms
diff_time_ms2sec() {
    local _tst_diff_time=$(($2 - $1))
    local _tst_time_sec=$((_tst_diff_time / 1000))
    local _tst_time_ms=$((_tst_diff_time % 1000))
    printf "%d.%03d" $_tst_time_sec $_tst_time_ms
}

# 获取测试套名
get_suite_name() {
    basename "$TST_TS_TOPDIR"
}

# 获取用例属性
# $1 -- 属性名称
# $2 -- 用例文件
get_case_attr() {
    local _attr
    # 先看用例文件
    if [ -n "$2" ]; then
        _attr="$(grep "@${1}:" "$2" 2>/dev/null | head -n 1 |
            sed "s|.*@${1}:\s*||g" | sed "s|\s*$||g")"
    elif [ -n "$TST_TC_FILE_FULL" ]; then
        _attr="$(grep "@${1}:" "$TST_TC_FILE_FULL" 2>/dev/null | head -n 1 |
            sed "s|.*@${1}:\s*||g" | sed "s|\s*$||g")"
    fi
    if [ -z "$_attr" ] && [ -f "${TST_TS_TOPDIR}/README.md" ]; then
        _attr="$(grep "^[[:blank:]]*@${1}:" "${TST_TS_TOPDIR}/README.md" 2>/dev/null | head -n 1 |
            sed "s|.*@${1}:\s*||g" | sed "s|\s*$||g")"
    fi
    echo "$_attr"
}

# 功能：设置环境变量
# 参数：
#   $1 -- 环境变量名
#   $2 -- 环境变量值
# 返回值：
#   0 -- 环境变量设置成功
#   1 -- 环境变量设置失败
setup_env_var() {
    local _tst_var_name="$1"
    local _tst_old_value
    eval _tst_old_value=\"\$"${1}"\"
    local _tst_new_value="$2"

    if ! echo "$_tst_var_name" | grep "^[A-Z_]\+$" >/dev/null; then
        msg "the var name must be upper and '_'"
        return 1
    fi
    [ -n "$_tst_old_value" ] && msg "the environment variable $_tst_var_name has old value: $_tst_old_value"
    eval export "$_tst_var_name"=\""$_tst_new_value"\"
    echo "export $_tst_var_name=\"$_tst_new_value\"" >>"$TST_TS_SYSDIR/environment_variable"
    return 0
}

# 功能：将K/M/G/T等单位互相转换
# 参数：wait_proc_exit -p pid [-t timeout] [-s signal]
#   -p pid 【必选参数】进程pid
#   -t timeout 【可选参数】等待超时时间（单位：秒，不指定时默认等待60秒），如果等待超时进程仍未退出则返回失败
#   -s signal 【可选参数】必须配合-t参数使用，超时时间到期后发送-s指定信号到进程
# 返回值：
#   0 -- 进程退出
#   1 -- 等待失败
wait_proc_exit() {
    local _tst_all_args="$*"
    local _tst_pid
    local _tst_timeout
    local _tst_signal
    while [ $# -gt 0 ]; do
        case "$1" in
            "-p")
                shift
                _tst_pid="$1"
                shift
                ;;
            "-t")
                shift
                _tst_timeout="$1"
                shift
                ;;
            "-s")
                shift
                _tst_signal="$1"
                shift
                ;;
            *)
                msg "unknown args $1 in $_tst_all_args"
                return 1
                ;;
        esac
    done
    if [ -z "$_tst_pid" ]; then
        msg "pid not set"
        return 1
    fi
    [ -z "$_tst_timeout" ] && _tst_timeout=60

    local _tst_time_start
    _tst_time_start="$(get_uptime)"
    local _tst_time_now
    _tst_time_now="$(get_uptime)"
    while [ $((_tst_time_start + _tst_timeout)) -gt "$_tst_time_now" ]; do
        [ -d "/proc/$_tst_pid" ] || return 0
        sleep 1
        _tst_time_now="$(get_uptime)"
    done
    [ -n "$_tst_signal" ] && kill -s "$_tst_signal" "$_tst_pid"
    sleep 1
    [ -d "/proc/$_tst_pid" ] || return 0
    return 1
}

# 功能：获取系统启动后到现在的时间，单位：秒
# 参数：无
# 返回值：标准输出时间
get_uptime() {
    awk -F . '{print $1}' /proc/uptime
}

# 功能：将K/M/G/T等单位互相转换
# 参数：conv_unit [-i k|m|g|t] [-o k|m|g|t] value[_with_unit]
#   -i k|m|g|t 【可选参数】输入数据的单位，可选参数，若不指定单位，则默认为1，或者输入的值后面带参数
#   -o k|m|g|t 【可选参数】输出数据的单位，若不指定单位，则默认为1
#   value[_with_unit] 【必选参数】需要转换的数据值，可以跟单位（只取单位第一个字母用于判断k|m|g|t）
# 返回值：标准输出转换后的结果
#   0 -- 转换成功
#   1 -- 转换失败
conv_unit() {
    local _tst_input_all
    local _tst_input_value
    local _tst_input_unit
    local _tst_output_value
    local _tst_output_unit
    while [ $# -gt 0 ]; do
        case "$1" in
            "-i")
                shift
                _tst_input_unit="$1"
                shift
                ;;
            "-o")
                shift
                _tst_output_unit="$1"
                shift
                ;;
            *)
                _tst_input_all="$_tst_input_all $1"
                shift
                ;;
        esac
    done
    # shellcheck disable=SC2001
    _tst_input_value=$(echo "$_tst_input_all" | sed "s|^[[:blank:]]*\([0-9]\+\)[[:blank:]]*\(.*\)|\1|g")
    # shellcheck disable=SC2001
    [ -z "$_tst_input_unit" ] && _tst_input_unit=$(echo "$_tst_input_all" |
        sed "s|^[[:blank:]]*\([0-9]\+\)[[:blank:]]*\(.*\)|\2|g" | head -c 1)
    [ -z "$_tst_input_unit" ] && _tst_input_unit="b"
    [ -z "$_tst_output_unit" ] && _tst_output_unit="b"
    if [ -z "$_tst_input_value" ]; then
        msg "no input value"
        return 1
    fi
    case "$_tst_input_unit" in
        b | B) ;;
        k | K)
            _tst_input_value=$((_tst_input_value * 1024))
            ;;
        m | M)
            _tst_input_value=$((_tst_input_value * 1024 * 1024))
            ;;
        g | G)
            _tst_input_value=$((_tst_input_value * 1024 * 1024 * 1024))
            ;;
        t | T)
            _tst_input_value=$((_tst_input_value * 1024 * 1024 * 1024 * 1024))
            ;;
        *)
            msg "unknown input unit $_tst_input_unit"
            return 1
            ;;
    esac
    case "$_tst_output_unit" in
        b | B)
            _tst_output_value=$_tst_input_value
            ;;
        k | K)
            _tst_output_value=$((_tst_input_value / 1024))
            ;;
        m | M)
            _tst_output_value=$((_tst_input_value / 1024 / 1024))
            ;;
        g | G)
            _tst_output_value=$((_tst_input_value / 1024 / 1024 / 1024))
            ;;
        t | T)
            _tst_output_value=$((_tst_input_value / 1024 / 1024 / 1024 / 1024))
            ;;
        *)
            msg "unknown output unit $_tst_output_unit"
            return 1
            ;;
    esac
    echo "$_tst_output_value"
    return 0
}

# 功能：ssh到环境子网中指定机器上
# 参数：
#   $1 -- 需要登录的机器IP编号，IP说明参考：https://iwiki.woa.com/pages/viewpage.action?pageId=1554994726
#   $* -- 要远程执行的命令及参数
# 返回值：同ssh
env_ssh() {
    local _tst_ip="${TST_VM_SUBNET}.$1"
    shift
    msg "try ssh to $_tst_ip execute: $*"
    ssh -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@"$_tst_ip" "$@"
}
env_ssh_timeout() {
    local _timeout=$1
    shift
    local _tst_ip="${TST_VM_SUBNET}.$1"
    shift
    msg "try ssh to $_tst_ip execute: $*"
    timeout -s SIGTERM "$_timeout" ssh -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
        root@"$_tst_ip" "$@"
}

# 功能：scp本机的文件到到环境子网中指定机器的指定路径
# 参数：
#   $1 -- scp目标机器IP编号，IP说明参考：https://iwiki.woa.com/pages/viewpage.action?pageId=1554994726
#   $2 -- 远程机器的目标文件夹
#   $* -- 需要从本机拷贝到远端机器的文件，可以拷贝多个文件
# 返回值：同scp
env_scpt() {
    local _tst_ip="${TST_VM_SUBNET}.$1"
    shift
    local _tst_target="$1"
    shift
    msg "try scp $* to ${_tst_ip}:$_tst_target"
    scp -rvq -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$@" root@"$_tst_ip":"$_tst_target"
}

# 功能：scp指定机器的文件到本机路径
# 参数：
#   $1 -- scp目标机器IP编号，IP说明参考：https://iwiki.woa.com/pages/viewpage.action?pageId=1554994726
#   $2 -- 本机目标文件夹
#   $* -- 需要从远端机器拷贝到本机的文件，可以拷贝多个文件
# 返回值：同scp
env_scpf() {
    local _tst_ret=0
    local _tst_ip="${TST_VM_SUBNET}.$1"
    shift
    local _tst_target="$1"
    shift
    while [ -n "$1" ]; do
        msg "try scp ${_tst_ip}:$1 to local $_tst_target"
        scp -rvq -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
            root@"$_tst_ip":"$1" "$_tst_target" || _tst_ret=1
        shift
    done
    return $_tst_ret
}

# 功能：等待指定机器就绪，默认超时时间60秒
# 参数：
#   $1 -- 目标机器IP编号，IP说明参考：https://iwiki.woa.com/pages/viewpage.action?pageId=1554994726
#   $2 -- 【可选】等待的超时时间，默认等待60秒
# 返回值：
#   0 -- 机器在超时前就绪
#   1 -- 机器启动异常
env_wait() {
    local _tst_start_time
    _tst_start_time=$(get_uptime)
    local _tst_end_time
    _tst_end_time=$(get_uptime)
    local _tst_timeout
    if [ -z "$2" ]; then
        _tst_timeout=60
    else
        _tst_timeout=$2
    fi

    local _tst_ip="${TST_VM_SUBNET}.$1"
    msg "wait env $_tst_ip standby"
    while [ $((_tst_start_time + _tst_timeout)) -ge "$_tst_end_time" ]; do
        echo -n "."
        sleep 3
        _tst_end_time=$(get_uptime)
        ping -w 1 -W 1 -c 1 "$_tst_ip" >/dev/null 2>&1 || continue
        if env_ssh_timeout 3 "$1" test -d /proc; then
            msg "the env with IP $_tst_ip ssh success, wait total $((_tst_end_time - _tst_start_time)) seconds"
            return 0
        else
            continue
        fi
    done
    _tst_end_time=$(get_uptime)
    msg "wait the env $_tst_ip timeout, wait total $((_tst_end_time - _tst_start_time)) seconds"
    return 1
}

# 功能：上传指定文件用于用例失败后的调试分析
# 参数：
#   $* -- 要上传的调试文件路径
# 返回值：
#   0 -- 上传成功
#   1 -- 上传失败
upload_debug_file() {
    local _tst_ret=0
    local _tst_file_name
    mkdir -p "${TST_TC_SYSDIR}/log_files"
    for f in "$@"; do
        _tst_file_name="$(date '+%Y%m%d-%H%M%S.%N')-$(basename "$f")"
        cp -rfv "$f" "${TST_TC_SYSDIR}/log_files/$_tst_file_name" || _tst_ret=1
    done
    return $_tst_ret
}

# 功能：获取发行版名称
# 返回值：输出ts/tss/oc/ocs
get_release_name() {
    if grep -iq "^[[:blank:]]*NAME=.*tencent.*stream" /etc/os-release; then
        echo "tss"
    elif grep -iq "^[[:blank:]]*NAME=.*tencent" /etc/os-release; then
        echo "ts"
    elif grep -iq "^[[:blank:]]*NAME=.*OpenCloudOS.*stream" /etc/os-release; then
        echo "ocs"
    elif grep -iq "^[[:blank:]]*NAME=.*OpenCloudOS" /etc/os-release; then
        echo "oc"
    else
        echo "unknown"
    fi
}
# 功能：获取发行版版本
# 返回值：输出ts/tss/oc/ocs
get_release_version() {
    grep "^[[:blank:]]*VERSION_ID=" /etc/os-release | tail -n 1 | awk -F = '{print $2}' | tr -d "'\""
}
# 功能：获取发行版名称
# 返回值：输出uname -r的结果
get_kernel_version() {
    uname -r
}
# 功能：获取系统架构
# 返回值：输出uname -m的结果
get_arch() {
    uname -m
}
# 功能：判断发行版是否private版本
# 返回值：
#   0 -- 是private版本
#   1 -- 是public版本
is_release_private() {
    [ -f /etc/imgversion ] && grep -iq "tencentos.*server.*-Tencent-" /etc/imgversion && return 0
    local _n
    local _v
    _n="$(get_release_name)"
    _v="$(get_release_version)"
    case "${_n}-${_v}" in
        "ts-2.6")
            return 0
            ;;
        "ts-3.2")
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}
# 功能：判断内核是否private版本
# 返回值：
#   0 -- 是private版本
#   1 -- 是public版本
is_kernel_private() {
    if uname -r | grep -iq tlinux; then
        return 0
    fi
    # private镜像内核为private
    [ -f /etc/imgversion ] && grep -iq "tencentos.*server.*-Tencent-" /etc/imgversion && return 0
    # TK5强制要求签名的也是private
    if uname -r | grep -q "^6\.6\."; then
        grep -wq module.sig_enforce=1 /proc/cmdline && return 0
    fi
    return 1
}
